/* 
 * Copyright 2001-2009 Terracotta, Inc. 
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not 
 * use this file except in compliance with the License. You may obtain a copy 
 * of the License at 
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0 
 *   
 * Unless required by applicable law or agreed to in writing, software 
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 
 * License for the specific language governing permissions and limitations 
 * under the License.
 * 
 */

package org.go.trigger.calendar;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.TimeZone;

import org.go.trigger.Calendar;

class CalendarComparator implements Comparator, Serializable {
	/**
	 * 
	 */
	private static final long serialVersionUID = 3599311260264934231L;

	public CalendarComparator() {
	}

	/** 
	 * @see java.util.Comparator#compare(java.lang.Object, java.lang.Object)
	 */
	@Override
	public int compare(Object arg0, Object arg1) {
		java.util.Calendar c1 = (java.util.Calendar) arg0;
		java.util.Calendar c2 = (java.util.Calendar) arg1;

		int month1 = c1.get(java.util.Calendar.MONTH);
		int month2 = c2.get(java.util.Calendar.MONTH);

		int day1 = c1.get(java.util.Calendar.DAY_OF_MONTH);
		int day2 = c2.get(java.util.Calendar.DAY_OF_MONTH);

		if (month1 < month2) {
			return -1;
		}
		if (month1 > month2) {
			return 1;
		}
		if (day1 < day2) {
			return -1;
		}
		if (day1 > day2) {
			return 1;
		}
		return 0;
	}
}

/**
 * <p>
 * This implementation of the Calendar excludes a set of days of the year. You
 * may use it to exclude bank holidays which are on the same date every year.
 * </p>
 * 
 * @see org.quartz.Calendar
 * @see org.go.trigger.calendar.BaseCalendar
 * 
 * @author Juergen Donnerstag
 */
public class AnnualCalendar extends BaseCalendar {

	@SuppressWarnings("hiding")
	static final long serialVersionUID = 7346867105876610961L;

	// true, if excludeDays is sorted
	private boolean dataSorted = false;

	private ArrayList<java.util.Calendar> excludeDays = new ArrayList<java.util.Calendar>();

	public AnnualCalendar() {
	}

	public AnnualCalendar(Calendar baseCalendar) {
		super(baseCalendar);
	}

	public AnnualCalendar(Calendar baseCalendar, TimeZone timeZone) {
		super(baseCalendar, timeZone);
	}

	public AnnualCalendar(TimeZone timeZone) {
		super(timeZone);
	}

	private void removeExcludedDay(java.util.Calendar day, boolean isChecked) {
		if (!isChecked && !isDayExcluded(day)) {
			return;
		}

		// Fast way, see if exact day object was already in list
		if (this.excludeDays.remove(day)) {
			return;
		}

		int dmonth = day.get(java.util.Calendar.MONTH);
		int dday = day.get(java.util.Calendar.DAY_OF_MONTH);

		// Since there is no guarantee that the given day is in the arraylist with the exact same year
		// search for the object based on month and day of month in the list and remove it
		Iterator iter = excludeDays.iterator();
		while (iter.hasNext()) {
			java.util.Calendar cl = (java.util.Calendar) iter.next();

			if (dmonth != cl.get(java.util.Calendar.MONTH)) {
				continue;
			}

			if (dday != cl.get(java.util.Calendar.DAY_OF_MONTH)) {
				continue;
			}

			day = cl;
			break;
		}

		this.excludeDays.remove(day);
	}

	@Override
	public Object clone() {
		AnnualCalendar clone = (AnnualCalendar) super.clone();
		clone.excludeDays = new ArrayList<java.util.Calendar>(excludeDays);
		return clone;
	}

	/**
	 * <p>
	 * Get the array which defines the exclude-value of each day of month
	 * </p>
	 */
	public ArrayList<java.util.Calendar> getDaysExcluded() {
		return excludeDays;
	}

	/**
	 * <p>
	 * Determine the next time (in milliseconds) that is 'included' by the
	 * Calendar after the given time. Return the original value if timeStamp is
	 * included. Return 0 if all days are excluded.
	 * </p>
	 * 
	 * <p>
	 * Note that this Calendar is only has full-day precision.
	 * </p>
	 */
	@Override
	public long getNextIncludedTime(long timeStamp) {
		// Call base calendar implementation first
		long baseTime = super.getNextIncludedTime(timeStamp);
		if ((baseTime > 0) && (baseTime > timeStamp)) {
			timeStamp = baseTime;
		}

		// Get timestamp for 00:00:00
		java.util.Calendar day = getStartOfDayJavaCalendar(timeStamp);
		if (isDayExcluded(day) == false) {
			return timeStamp; // return the original value
		}

		while (isDayExcluded(day) == true) {
			day.add(java.util.Calendar.DATE, 1);
		}

		return day.getTime().getTime();
	}

	/**
	 * <p>
	 * Return true, if day is defined to be exluded.
	 * </p>
	 */
	public boolean isDayExcluded(java.util.Calendar day) {

		if (day == null) {
			throw new IllegalArgumentException("Parameter day must not be null");
		}

		// Check baseCalendar first
		if (!super.isTimeIncluded(day.getTime().getTime())) {
			return true;
		}

		int dmonth = day.get(java.util.Calendar.MONTH);
		int dday = day.get(java.util.Calendar.DAY_OF_MONTH);

		if (dataSorted == false) {
			Collections.sort(excludeDays, new CalendarComparator());
			dataSorted = true;
		}

		Iterator iter = excludeDays.iterator();
		while (iter.hasNext()) {
			java.util.Calendar cl = (java.util.Calendar) iter.next();

			// remember, the list is sorted
			if (dmonth < cl.get(java.util.Calendar.MONTH)) {
				return false;
			}

			if (dday != cl.get(java.util.Calendar.DAY_OF_MONTH)) {
				continue;
			}

			if (dmonth != cl.get(java.util.Calendar.MONTH)) {
				continue;
			}

			return true;
		}

		return false;
	}

	/**
	 * <p>
	 * Determine whether the given time (in milliseconds) is 'included' by the
	 * Calendar.
	 * </p>
	 * 
	 * <p>
	 * Note that this Calendar is only has full-day precision.
	 * </p>
	 */
	@Override
	public boolean isTimeIncluded(long timeStamp) {
		// Test the base calendar first. Only if the base calendar not already
		// excludes the time/date, continue evaluating this calendar instance.
		if (super.isTimeIncluded(timeStamp) == false) {
			return false;
		}

		java.util.Calendar day = createJavaCalendar(timeStamp);

		return !(isDayExcluded(day));
	}

	/**
	 * Remove the given day from the list of excluded days
	 *  
	 * @param day
	 * @return
	 */
	public void removeExcludedDay(java.util.Calendar day) {
		removeExcludedDay(day, false);
	}

	/**
	 * <p>
	 * Redefine a certain day to be excluded (true) or included (false).
	 * </p>
	 */
	public void setDayExcluded(java.util.Calendar day, boolean exclude) {
		if (exclude) {
			if (isDayExcluded(day)) {
				return;
			}

			excludeDays.add(day);
			dataSorted = false;
		} else {
			if (!isDayExcluded(day)) {
				return;
			}

			removeExcludedDay(day, true);
		}
	}

	/**
	 * <p>
	 * Redefine the list of days excluded. The ArrayList 
	 * should contain <code>java.util.Calendar</code> objects. 
	 * </p>
	 */
	public void setDaysExcluded(ArrayList days) {
		if (days == null) {
			excludeDays = new ArrayList();
		} else {
			excludeDays = days;
		}

		dataSorted = false;
	}
}
